/*
 *  Workspace window manager
 *  Copyright (c) 2015-2021 Sergii Stoian
 *
 *  WINGs library (Window Maker)
 *  Copyright (c) 1998 scottc
 *  Copyright (c) 1999-2004 Dan Pascu
 *  Copyright (c) 1999-2000 Alfredo K. Kojima
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <stdlib.h>
#include <string.h>

#include "WM.h"
#include "WMcore.h"
#include "util.h"
#include "log_utils.h"
#include "string_utils.h"

#include "wscreen.h"
#include "dragcommon.h"
#include "wevent.h"
#include "selection.h"
#include "wpixmap.h"
#include "drawing.h"

#include <X11/Xatom.h>
#include <X11/cursorfont.h>
#ifdef USE_XSHAPE
#include <X11/extensions/shape.h>
#endif

#define XDND_DESTINATION_RESPONSE_MAX_DELAY 10000
#define MIN_X_MOVE_OFFSET 5
#define MIN_Y_MOVE_OFFSET 5
#define MAX_SLIDEBACK_ITER 15

#define XDND_PROPERTY_FORMAT 32
#define XDND_ACTION_DESCRIPTION_FORMAT 8

#define XDND_DEST_VERSION(dragInfo) dragInfo->protocolVersion
#define XDND_SOURCE_INFO(dragInfo) dragInfo->sourceInfo
#define XDND_DEST_WIN(dragInfo) dragInfo->sourceInfo->destinationWindow
#define XDND_SOURCE_ACTION(dragInfo) dragInfo->sourceAction
#define XDND_DEST_ACTION(dragInfo) dragInfo->destinationAction
#define XDND_SOURCE_VIEW(dragInfo) dragInfo->sourceInfo->sourceView
#define XDND_SOURCE_STATE(dragInfo) dragInfo->sourceInfo->state
#define XDND_SELECTION_PROCS(dragInfo) dragInfo->sourceInfo->selectionProcs
#define XDND_DRAG_ICON(dragInfo) dragInfo->sourceInfo->icon
#define XDND_MOUSE_OFFSET(dragInfo) dragInfo->sourceInfo->mouseOffset
#define XDND_DRAG_CURSOR(dragInfo) dragInfo->sourceInfo->dragCursor
#define XDND_DRAG_ICON_POS(dragInfo) dragInfo->sourceInfo->imageLocation
#define XDND_NO_POS_ZONE(dragInfo) dragInfo->sourceInfo->noPositionMessageZone
#define XDND_TIMESTAMP(dragInfo) dragInfo->timestamp
#define XDND_3_TYPES(dragInfo) dragInfo->sourceInfo->firstThreeTypes
#define XDND_SOURCE_VIEW_STORED(dragInfo) dragInfo->sourceInfo != NULL  \
    && dragInfo->sourceInfo->sourceView != NULL

static CFRunLoopTimerRef dndSourceTimer = NULL;

static void *idleState(WMView *srcView, XClientMessageEvent *event, WMDraggingInfo *info);
static void *dropAllowedState(WMView *srcView, XClientMessageEvent *event, WMDraggingInfo *info);
static void *finishDropState(WMView *srcView, XClientMessageEvent *event, WMDraggingInfo *info);

#ifdef XDND_DEBUG
static const char *stateName(WMDndState *state)
{
  if (state == NULL)
    return "no state defined";

  if (state == idleState)
    return "idleState";

  if (state == dropAllowedState)
    return "dropAllowedState";

  if (state == finishDropState)
    return "finishDropState";

  return "unknown state";
}
#endif

static WMScreen *sourceScreen(WMDraggingInfo *info)
{
  return WMViewScreen(XDND_SOURCE_VIEW(info));
}

static void endDragProcess(WMDraggingInfo *info, Bool deposited)
{
  WMView *view = XDND_SOURCE_VIEW(info);
  WMScreen *scr = WMViewScreen(XDND_SOURCE_VIEW(info));

  /* free selection handler while view exists */
  WMDeleteSelectionHandler(view, scr->xdndSelectionAtom, CurrentTime);
  wfree(XDND_SELECTION_PROCS(info));

  if (XDND_DRAG_CURSOR(info) != None) {
    XFreeCursor(scr->display, XDND_DRAG_CURSOR(info));
    XDND_DRAG_CURSOR(info) = None;
  }

  if (view->dragSourceProcs->endedDrag != NULL) {
    /* this can destroy source view (with a "move" action for example) */
    view->dragSourceProcs->endedDrag(view, &XDND_DRAG_ICON_POS(info), deposited);
  }

  /* clear remaining draggging infos */
  wfree(XDND_SOURCE_INFO(info));
  XDND_SOURCE_INFO(info) = NULL;
}

/* ----- drag cursor ----- */
static void initDragCursor(WMDraggingInfo *info)
{
  WMScreen *scr = sourceScreen(info);
  XColor cursorFgColor, cursorBgColor;

  /* green */
  cursorFgColor.red = 0x4500;
  cursorFgColor.green = 0xb000;
  cursorFgColor.blue = 0x4500;

  /* white */
  cursorBgColor.red = 0xffff;
  cursorBgColor.green = 0xffff;
  cursorBgColor.blue = 0xffff;

  XDND_DRAG_CURSOR(info) = XCreateFontCursor(scr->display, XC_left_ptr);
  XRecolorCursor(scr->display, XDND_DRAG_CURSOR(info), &cursorFgColor, &cursorBgColor);

  XFlush(scr->display);
}

static void recolorCursor(WMDraggingInfo *info, Bool dropIsAllowed)
{
  WMScreen *scr = sourceScreen(info);

  if (dropIsAllowed) {
    XDefineCursor(scr->display, scr->rootWin, XDND_DRAG_CURSOR(info));
  } else {
    XDefineCursor(scr->display, scr->rootWin, scr->defaultCursor);
  }

  XFlush(scr->display);
}

/* ----- end of drag cursor ----- */

/* ----- selection procs ----- */
static WMData *convertSelection(WMView *view, Atom selection, Atom target, void *cdata, Atom *type)
{
  WMScreen *scr;
  WMData *data;
  char *typeName;

  /* Parameter not used, but tell the compiler that it is ok */
  (void) selection;
  (void) cdata;

  scr = WMViewScreen(view);
  typeName = XGetAtomName(scr->display, target);

  *type = target;

  if (view->dragSourceProcs->fetchDragData != NULL) {
    data = view->dragSourceProcs->fetchDragData(view, typeName);
  } else {
    data = NULL;
  }

  if (typeName != NULL)
    XFree(typeName);

  return data;
}

static void selectionLost(WMView *view, Atom selection, void *cdata)
{
  /* Parameter not used, but tell the compiler that it is ok */
  (void) view;
  (void) selection;
  (void) cdata;

  WMLogWarning(_("XDND selection lost during drag operation..."));
}

static void selectionDone(WMView *view, Atom selection, Atom target, void *cdata)
{
  /* Parameter not used, but tell the compiler that it is ok */
  (void) view;
  (void) selection;
  (void) target;
  (void) cdata;

#ifdef XDND_DEBUG
  printf("selection done\n");
#endif
}

/* ----- end of selection procs ----- */

/* ----- visual part ----- */

static Window makeDragIcon(WMScreen *scr, WMPixmap *pixmap)
{
  Window window;
  WMSize size;
  unsigned long flags;
  XSetWindowAttributes attribs;

  if (!pixmap) {
    pixmap = scr->defaultObjectIcon;
  }

  size = WMGetPixmapSize(pixmap);

  flags = CWSaveUnder | CWBackPixmap | CWOverrideRedirect | CWColormap;
  attribs.save_under = True;
  attribs.background_pixmap = pixmap->pixmap;
  attribs.override_redirect = True;
  attribs.colormap = scr->colormap;

  window = XCreateWindow(scr->display, scr->rootWin, 0, 0, size.width,
                         size.height, 0, scr->depth, InputOutput, scr->visual, flags, &attribs);

#ifdef USE_XSHAPE
  if (pixmap->mask) {
    XShapeCombineMask(scr->display, window, ShapeBounding, 0, 0, pixmap->mask, ShapeSet);
  }
#endif

  return window;
}

static void slideWindow(Display *dpy, Window win, int srcX, int srcY, int dstX, int dstY)
{
  double x, y, dx, dy;
  int i;
  int iterations;

  iterations = WMIN(MAX_SLIDEBACK_ITER, WMAX(abs(dstX - srcX), abs(dstY - srcY)));

  x = srcX;
  y = srcY;

  dx = (double)(dstX - srcX) / iterations;
  dy = (double)(dstY - srcY) / iterations;

  for (i = 0; i <= iterations; i++) {
    XMoveWindow(dpy, win, x, y);
    XFlush(dpy);

    wusleep(800);

    x += dx;
    y += dy;
  }
}

static int getInitialDragImageCoord(int viewCoord, int mouseCoord, int viewSize, int iconSize)
{
  if (iconSize >= viewSize) {
    /* center icon coord on view */
    return viewCoord - iconSize / 2;
  } else {
    /* try to center icon on mouse pos */

    if (mouseCoord - iconSize / 2 <= viewCoord)
      /* if icon was centered on mouse, it would be off view
         thus, put icon left (resp. top) side
         at view (resp. top) side */
      return viewCoord;

    else if (mouseCoord + iconSize / 2 >= viewCoord + viewSize)
      /* if icon was centered on mouse, it would be off view
         thus, put icon right (resp. bottom) side
         at view right (resp. bottom) side */
      return viewCoord + viewSize - iconSize;

    else
      return mouseCoord - iconSize / 2;
  }

}

static void initDragImagePos(WMView *view, WMDraggingInfo *info, XEvent *event)
{
  WMSize iconSize = WMGetPixmapSize(view->dragImage);
  WMSize viewSize = WMGetViewSize(view);
  WMPoint viewPos;
  Window foo;

  XTranslateCoordinates(WMViewScreen(view)->display,
                        WMViewXID(view), WMViewScreen(view)->rootWin,
                        0, 0, &(viewPos.x), &(viewPos.y), &foo);

  /* set icon pos */
  XDND_DRAG_ICON_POS(info).x =
    getInitialDragImageCoord(viewPos.x, event->xmotion.x_root, viewSize.width, iconSize.width);

  XDND_DRAG_ICON_POS(info).y =
    getInitialDragImageCoord(viewPos.y, event->xmotion.y_root, viewSize.height, iconSize.height);

  /* set mouse offset relative to icon */
  XDND_MOUSE_OFFSET(info).x = event->xmotion.x_root - XDND_DRAG_ICON_POS(info).x;
  XDND_MOUSE_OFFSET(info).y = event->xmotion.y_root - XDND_DRAG_ICON_POS(info).y;
}

static void refreshDragImage(WMView *view, WMDraggingInfo *info)
{
  WMScreen *scr = WMViewScreen(view);

  XMoveWindow(scr->display, XDND_DRAG_ICON(info), XDND_DRAG_ICON_POS(info).x, XDND_DRAG_ICON_POS(info).y);
}

static void startDragImage(WMView *view, WMDraggingInfo *info, XEvent *event)
{
  WMScreen *scr = WMViewScreen(view);

  XDND_DRAG_ICON(info) = makeDragIcon(scr, view->dragImage);
  initDragImagePos(view, info, event);
  refreshDragImage(view, info);
  XMapRaised(scr->display, XDND_DRAG_ICON(info));

  initDragCursor(info);
}

static void endDragImage(WMDraggingInfo *info, Bool slideBack)
{
  WMView *view = XDND_SOURCE_VIEW(info);
  Display *dpy = WMViewScreen(view)->display;

  if (slideBack) {
    WMPoint toLocation;
    Window foo;

    XTranslateCoordinates(WMViewScreen(view)->display,
                          WMViewXID(view), WMViewScreen(view)->rootWin,
                          0, 0, &(toLocation.x), &(toLocation.y), &foo);

    slideWindow(dpy, XDND_DRAG_ICON(info),
                XDND_DRAG_ICON_POS(info).x, XDND_DRAG_ICON_POS(info).y, toLocation.x, toLocation.y);
  }

  XDestroyWindow(dpy, XDND_DRAG_ICON(info));
}

/* ----- end of visual part ----- */

/* ----- messages ----- */

/* send a DnD message to the destination window */
static Bool
sendDnDClientMessage(WMDraggingInfo *info, Atom message,
		     unsigned long data1, unsigned long data2, unsigned long data3, unsigned long data4)
{
  Display *dpy = sourceScreen(info)->display;
  Window srcWin = WMViewXID(XDND_SOURCE_VIEW(info));
  Window destWin = XDND_DEST_WIN(info);

  if (!WMSendDnDClientMessage(dpy, destWin, message, srcWin, data1, data2, data3, data4)) {
    /* drop failed */
    recolorCursor(info, False);
    endDragImage(info, True);
    endDragProcess(info, False);
    return False;
  }

  return True;
}

static Bool sendEnterMessage(WMDraggingInfo *info)
{
  WMScreen *scr = sourceScreen(info);
  unsigned long version;

  if (XDND_DEST_VERSION(info) > 2) {
    if (XDND_DEST_VERSION(info) < XDND_VERSION)
      version = XDND_DEST_VERSION(info);
    else
      version = XDND_VERSION;
  } else {
    version = 3;
  }

  return sendDnDClientMessage(info, scr->xdndEnterAtom, (version << 24) | 1,	/* 1: support of type list */
                              XDND_3_TYPES(info)[0], XDND_3_TYPES(info)[1], XDND_3_TYPES(info)[2]);
}

static Bool sendPositionMessage(WMDraggingInfo *info, WMPoint *mousePos)
{
  WMScreen *scr = sourceScreen(info);
  WMRect *noPosZone = &(XDND_NO_POS_ZONE(info));

  if (noPosZone->size.width != 0 || noPosZone->size.height != 0) {
    if (mousePos->x < noPosZone->pos.x || mousePos->x > (noPosZone->pos.x + noPosZone->size.width)
        || mousePos->y < noPosZone->pos.y || mousePos->y > (noPosZone->pos.y + noPosZone->size.height)) {
      /* send position if out of zone defined by destination */
      return sendDnDClientMessage(info, scr->xdndPositionAtom,
                                  0,
                                  mousePos->x << 16 | mousePos->y,
                                  XDND_TIMESTAMP(info), XDND_SOURCE_ACTION(info));
    }

    /* Nothing to send, always succeed */
    return True;

  }

  /* send position on each move */
  return sendDnDClientMessage(info, scr->xdndPositionAtom,
                              0,
                              mousePos->x << 16 | mousePos->y,
                              XDND_TIMESTAMP(info), XDND_SOURCE_ACTION(info));
}

static Bool sendLeaveMessage(WMDraggingInfo *info)
{
  WMScreen *scr = sourceScreen(info);

  return sendDnDClientMessage(info, scr->xdndLeaveAtom, 0, 0, 0, 0);
}

static Bool sendDropMessage(WMDraggingInfo *info)
{
  WMScreen *scr = sourceScreen(info);

  return sendDnDClientMessage(info, scr->xdndDropAtom, 0, XDND_TIMESTAMP(info), 0, 0);
}

/* ----- end of messages ----- */

static Atom *getTypeAtomList(WMScreen *scr, WMView *view, int *count)
{
  CFMutableArrayRef types;
  Atom *typeAtoms;
  int i;

  types = view->dragSourceProcs->dropDataTypes(view);

  if (types != NULL) {
    *count = CFArrayGetCount(types);
    if (*count > 0) {
      typeAtoms = wmalloc((*count) *sizeof(Atom));
      for (i = 0; i < *count; i++) {
        typeAtoms[i] = XInternAtom(scr->display, CFArrayGetValueAtIndex(types, i), False);
      }

      /* WMFreeArray(types); */
      return typeAtoms;
    }

    /* WMFreeArray(types); */
  }

  *count = 1;
  typeAtoms = wmalloc(sizeof(Atom));
  *typeAtoms = None;

  return typeAtoms;
}

static void registerDropTypes(WMScreen *scr, WMView *view, WMDraggingInfo *info)
{
  Atom *typeList;
  int i, count;

  typeList = getTypeAtomList(scr, view, &count);

  /* store the first 3 types */
  for (i = 0; i < 3 && i < count; i++)
    XDND_3_TYPES(info)[i] = typeList[i];

  for (; i < 3; i++)
    XDND_3_TYPES(info)[i] = None;

  /* store the entire type list */
  XChangeProperty(scr->display,
                  WMViewXID(view),
                  scr->xdndTypeListAtom,
                  XA_ATOM, XDND_PROPERTY_FORMAT, PropModeReplace, (unsigned char *)typeList, count);
}

static void registerOperationList(WMScreen *scr, WMView *view, CFMutableArrayRef operationArray)
{
  Atom *actionList;
  WMDragOperationType operation;
  int count = CFArrayGetCount(operationArray);
  int i;

  actionList = wmalloc(sizeof(Atom) *count);

  for (i = 0; i < count; i++) {
    operation = WMGetDragOperationItemType((WMDragOperationItem *)CFArrayGetValueAtIndex(operationArray, i));
    actionList[i] = WMOperationToAction(scr, operation);
  }

  XChangeProperty(scr->display,
                  WMViewXID(view),
                  scr->xdndActionListAtom,
                  XA_ATOM, XDND_PROPERTY_FORMAT, PropModeReplace, (unsigned char *)actionList, count);
}

static void registerDescriptionList(WMScreen *scr, WMView *view, CFArrayRef operationArray)
{
  char *text, *textListItem, *textList;
  int count = CFArrayGetCount(operationArray);
  int i;
  int size = 0;

  /* size of XA_STRING info */
  for (i = 0; i < count; i++) {
    size += strlen(WMGetDragOperationItemText((WMDragOperationItem *)CFArrayGetValueAtIndex(operationArray, i))) + 1 /* NULL */;
  }

  /* create text list */
  textList = wmalloc(size);
  textListItem = textList;

  for (i = 0; i < count; i++) {
    text = WMGetDragOperationItemText((WMDragOperationItem *)CFArrayGetValueAtIndex(operationArray, i));
    wstrlcpy(textListItem, text, size);

    /* to next text offset */
    textListItem = &(textListItem[strlen(textListItem) + 1]);
  }

  XChangeProperty(scr->display,
                  WMViewXID(view),
                  scr->xdndActionDescriptionAtom,
                  XA_STRING,
                  XDND_ACTION_DESCRIPTION_FORMAT, PropModeReplace, (unsigned char *)textList, size);
}

/* called if wanted operation is WDOperationAsk */
static void registerSupportedOperations(WMView *view)
{
  WMScreen *scr = WMViewScreen(view);
  CFMutableArrayRef operationArray;

  operationArray = view->dragSourceProcs->askedOperations(view);

  registerOperationList(scr, view, operationArray);
  registerDescriptionList(scr, view, operationArray);

  /* WMFreeArray(operationArray); */
}

static void initSourceDragInfo(WMView *sourceView, WMDraggingInfo *info)
{
  WMRect emptyZone;

  XDND_SOURCE_INFO(info) = (WMDragSourceInfo *) wmalloc(sizeof(WMDragSourceInfo));

  XDND_SOURCE_VIEW(info) = sourceView;
  XDND_DEST_WIN(info) = None;
  XDND_DRAG_ICON(info) = None;

  XDND_SOURCE_ACTION(info) = WMOperationToAction(WMViewScreen(sourceView),
                                                 sourceView->dragSourceProcs->
                                                 wantedDropOperation(sourceView));

  XDND_DEST_ACTION(info) = None;

  XDND_SOURCE_STATE(info) = idleState;

  emptyZone.pos = WMMakePoint(0, 0);
  emptyZone.size = WMMakeSize(0, 0);
  XDND_NO_POS_ZONE(info) = emptyZone;
}

/*
  Returned array is destroyed after dropDataTypes call
*/
static CFMutableArrayRef defDropDataTypes(WMView *self)
{
  /* Parameter not used, but tell the compiler that it is ok */
  (void) self;

  return NULL;
}

static WMDragOperationType defWantedDropOperation(WMView *self)
{
  /* Parameter not used, but tell the compiler that it is ok */
  (void) self;

  return WDOperationNone;
}

/*
  Must be defined if wantedDropOperation return WDOperationAsk
  (useless otherwise).
  Return a WMDragOperationItem array (destroyed after call).
  A WMDragOperationItem links a label to an operation.
  static CFMutableArrayRef defAskedOperations(WMView *self);
*/

static Bool defAcceptDropOperation(WMView *self, WMDragOperationType allowedOperation)
{
  /* Parameter not used, but tell the compiler that it is ok */
  (void) self;
  (void) allowedOperation;

  return False;
}

static void defBeganDrag(WMView *self, WMPoint *point)
{
  /* Parameter not used, but tell the compiler that it is ok */
  (void) self;
  (void) point;
}

static void defEndedDrag(WMView *self, WMPoint *point, Bool deposited)
{
  /* Parameter not used, but tell the compiler that it is ok */
  (void) self;
  (void) point;
  (void) deposited;
}

/*
  Returned data is not destroyed
*/
static WMData *defFetchDragData(WMView *self, char *type)
{
  /* Parameter not used, but tell the compiler that it is ok */
  (void) self;
  (void) type;

  return NULL;
}

void WMSetViewDragSourceProcs(WMView *view, WMDragSourceProcs *procs)
{
  if (view->dragSourceProcs)
    wfree(view->dragSourceProcs);
  view->dragSourceProcs = wmalloc(sizeof(WMDragSourceProcs));

  *view->dragSourceProcs = *procs;

  if (procs->dropDataTypes == NULL)
    view->dragSourceProcs->dropDataTypes = defDropDataTypes;

  if (procs->wantedDropOperation == NULL)
    view->dragSourceProcs->wantedDropOperation = defWantedDropOperation;

  /*
    Note: askedOperations can be NULL, if wantedDropOperation never returns
    WDOperationAsk.
  */

  if (procs->acceptDropOperation == NULL)
    view->dragSourceProcs->acceptDropOperation = defAcceptDropOperation;

  if (procs->beganDrag == NULL)
    view->dragSourceProcs->beganDrag = defBeganDrag;

  if (procs->endedDrag == NULL)
    view->dragSourceProcs->endedDrag = defEndedDrag;

  if (procs->fetchDragData == NULL)
    view->dragSourceProcs->fetchDragData = defFetchDragData;
}

static Bool isXdndAware(WMScreen *scr, Window win)
{
  Atom type;
  int format;
  unsigned long count, remain;
  unsigned char *winXdndVersion;

  if (win == None)
    return False;

  XGetWindowProperty(scr->display, win, scr->xdndAwareAtom,
                     0, 1, False, XA_ATOM, &type, &format, &count, &remain, &winXdndVersion);

  if (type != XA_ATOM || format != XDND_PROPERTY_FORMAT || count == 0 || !winXdndVersion) {
    if (winXdndVersion)
      XFree(winXdndVersion);
    return False;
  }

  XFree(winXdndVersion);
  return (count == 1);	/* xdnd version is set */
}

static Window *windowChildren(Display *dpy, Window win, unsigned *nchildren)
{
  Window *children;
  Window foo, bar;

  if (!XQueryTree(dpy, win, &foo, &bar, &children, nchildren)) {
    *nchildren = 0;
    return NULL;
  } else
    return children;
}

static Window lookForAwareWindow(WMScreen *scr, WMPoint *mousePos, Window win)
{
  int tmpx, tmpy;
  Window child;

  /* Since xdnd v3, only the toplevel window should be aware */
  if (isXdndAware(scr, win))
    return win;

  /* inspect child under pointer */
  if (XTranslateCoordinates(scr->display, scr->rootWin, win, mousePos->x, mousePos->y, &tmpx, &tmpy, &child)) {
    if (child == None)
      return None;
    else
      return lookForAwareWindow(scr, mousePos, child);
  }

  return None;
}

static Window findDestination(WMDraggingInfo *info, WMPoint *mousePos)
{
  WMScreen *scr = sourceScreen(info);
  unsigned nchildren;
  Window *children = windowChildren(scr->display, scr->rootWin, &nchildren);
  int i;
  XWindowAttributes attr;

  if (isXdndAware(scr, scr->rootWin))
    return scr->rootWin;

  /* exclude drag icon (and upper) from search */
  for (i = nchildren - 1; i >= 0; i--) {
    if (children[i] == XDND_DRAG_ICON(info)) {
      i--;
      break;
    }
  }

  if (i < 0) {
    /* root window has no child under drag icon, and is not xdnd aware. */
    return None;
  }

  /* inspecting children, from upper to lower */
  for (; i >= 0; i--) {
    if (XGetWindowAttributes(scr->display, children[i], &attr)
        && attr.map_state == IsViewable
        && mousePos->x >= attr.x
        && mousePos->y >= attr.y
        && mousePos->x < attr.x + attr.width && mousePos->y < attr.y + attr.height) {
      return lookForAwareWindow(scr, mousePos, children[i]);
    }
  }

  /* No child window under drag pointer */
  return None;
}

static void storeDestinationProtocolVersion(WMDraggingInfo *info)
{
  Atom type;
  int format;
  unsigned long count, remain;
  unsigned char *winXdndVersion;
  WMScreen *scr = WMViewScreen(XDND_SOURCE_VIEW(info));

  wassertr(XDND_DEST_WIN(info) != None);

  if (XGetWindowProperty(scr->display, XDND_DEST_WIN(info),
                         scr->xdndAwareAtom,
                         0, 1, False, XA_ATOM, &type, &format,
                         &count, &remain, &winXdndVersion) == Success) {
    XDND_DEST_VERSION(info) = *winXdndVersion;
    XFree(winXdndVersion);
  } else {
    XDND_DEST_VERSION(info) = 0;
    WMLogWarning(_("could not get XDND version for target of drop"));
  }
}

static void initMotionProcess(WMView *view, WMDraggingInfo *info, XEvent *event, WMPoint *startLocation)
{
  WMScreen *scr = WMViewScreen(view);

  /* take ownership of XdndSelection */
  XDND_SELECTION_PROCS(info) = (WMSelectionProcs *) wmalloc(sizeof(WMSelectionProcs));
  XDND_SELECTION_PROCS(info)->convertSelection = convertSelection;
  XDND_SELECTION_PROCS(info)->selectionLost = selectionLost;
  XDND_SELECTION_PROCS(info)->selectionDone = selectionDone;
  XDND_TIMESTAMP(info) = event->xmotion.time;

  if (!WMCreateSelectionHandler(view, scr->xdndSelectionAtom, CurrentTime, XDND_SELECTION_PROCS(info), NULL)) {
    WMLogWarning(_("could not get ownership of XDND selection"));
    return;
  }

  registerDropTypes(scr, view, info);

  if (XDND_SOURCE_ACTION(info) == WMViewScreen(view)->xdndActionAsk)
    registerSupportedOperations(view);

  if (view->dragSourceProcs->beganDrag != NULL) {
    view->dragSourceProcs->beganDrag(view, startLocation);
  }
}

static void processMotion(WMDraggingInfo *info, WMPoint *mousePos)
{
  Window newDestination = findDestination(info, mousePos);

  WMDragSourceStopTimer();

  if (newDestination != XDND_DEST_WIN(info)) {
    recolorCursor(info, False);

    if (XDND_DEST_WIN(info) != None) {
      /* leaving a xdnd window */
      sendLeaveMessage(info);
    }

    XDND_DEST_WIN(info) = newDestination;
    XDND_DEST_ACTION(info) = None;
    XDND_NO_POS_ZONE(info).size.width = 0;
    XDND_NO_POS_ZONE(info).size.height = 0;

    if (newDestination != None) {
      /* entering a xdnd window */
      XDND_SOURCE_STATE(info) = idleState;
      storeDestinationProtocolVersion(info);

      if (!sendEnterMessage(info)) {
        XDND_DEST_WIN(info) = None;
        return;
      }

      WMDragSourceStartTimer(info);
    } else {
      XDND_SOURCE_STATE(info) = NULL;
    }
  } else {
    if (XDND_DEST_WIN(info) != None) {
      if (!sendPositionMessage(info, mousePos)) {
        XDND_DEST_WIN(info) = None;
        return;
      }

      WMDragSourceStartTimer(info);
    }
  }
}

static Bool processButtonRelease(WMDraggingInfo *info)
{
  if (XDND_SOURCE_STATE(info) == dropAllowedState) {
    /* begin drop */
    WMDragSourceStopTimer();

    if (!sendDropMessage(info))
      return False;

    WMDragSourceStartTimer(info);
    return True;
  } else {
    if (XDND_DEST_WIN(info) != None)
      sendLeaveMessage(info);

    WMDragSourceStopTimer();
    return False;
  }
}

Bool WMIsDraggingFromView(WMView *view)
{
  WMDraggingInfo *info = WMViewScreen(view)->dragInfo;

  return (XDND_SOURCE_INFO(info) != NULL && XDND_SOURCE_STATE(info) != finishDropState);
  /* return WMViewScreen(view)->dragInfo.sourceInfo != NULL; */
}

void WMDragImageFromView(WMView *view, XEvent *event)
{
  WMDraggingInfo *info = WMViewScreen(view)->dragInfo;
  WMPoint mouseLocation;

  switch (event->type) {
  case ButtonPress:
    if (event->xbutton.button == Button1) {
      XEvent nextEvent;

      XPeekEvent(event->xbutton.display, &nextEvent);

      /* Initialize only if a drag really begins (avoid clicks) */
      if (nextEvent.type == MotionNotify) {
        initSourceDragInfo(view, info);
      }
    }

    break;

  case ButtonRelease:
    if (WMIsDraggingFromView(view)) {
      Bool dropBegan = processButtonRelease(info);

      recolorCursor(info, False);
      if (dropBegan) {
        endDragImage(info, False);
        XDND_SOURCE_STATE(info) = finishDropState;
      } else {
        /* drop failed */
        endDragImage(info, True);
        endDragProcess(info, False);
      }
    }
    break;

  case MotionNotify:
    if (WMIsDraggingFromView(view)) {
      mouseLocation = WMMakePoint(event->xmotion.x_root, event->xmotion.y_root);

      if (abs(XDND_DRAG_ICON_POS(info).x - mouseLocation.x) >=
          MIN_X_MOVE_OFFSET
          || abs(XDND_DRAG_ICON_POS(info).y - mouseLocation.y) >= MIN_Y_MOVE_OFFSET) {
        if (XDND_DRAG_ICON(info) == None) {
          initMotionProcess(view, info, event, &mouseLocation);
          startDragImage(view, info, event);
        } else {
          XDND_DRAG_ICON_POS(info).x = mouseLocation.x - XDND_MOUSE_OFFSET(info).x;
          XDND_DRAG_ICON_POS(info).y = mouseLocation.y - XDND_MOUSE_OFFSET(info).y;

          refreshDragImage(view, info);
          processMotion(info, &mouseLocation);
        }
      }
    }
    break;
  }
}

/* Minimal mouse events handler: no right or double-click detection,
   only drag is supported */
static void dragImageHandler(XEvent *event, void *cdata)
{
  WMView *view = (WMView *) cdata;

  WMDragImageFromView(view, event);
}

/* ----- source states ----- */

#ifdef XDND_DEBUG
static void traceStatusMsg(Display *dpy, XClientMessageEvent *statusEvent)
{
  printf("Xdnd status message:\n");

  if (statusEvent->data.l[1] & 0x2UL)
    printf("\tsend position on every move\n");
  else {
    int x, y, w, h;
    x = statusEvent->data.l[2] >> 16;
    y = statusEvent->data.l[2] & 0xFFFFL;
    w = statusEvent->data.l[3] >> 16;
    h = statusEvent->data.l[3] & 0xFFFFL;

    printf("\tsend position out of ((%d,%d) , (%d,%d))\n", x, y, x + w, y + h);
  }

  if (statusEvent->data.l[1] & 0x1L)
    printf("\tallowed action: %s\n", XGetAtomName(dpy, statusEvent->data.l[4]));
  else
    printf("\tno action allowed\n");
}
#endif

static void storeDropAction(WMDraggingInfo *info, Atom destAction)
{
  WMView *sourceView = XDND_SOURCE_VIEW(info);
  WMScreen *scr = WMViewScreen(sourceView);

  if (sourceView->dragSourceProcs->acceptDropOperation != NULL) {
    if (sourceView->dragSourceProcs->acceptDropOperation(sourceView,
                                                         WMActionToOperation(scr, destAction)))
      XDND_DEST_ACTION(info) = destAction;
    else
      XDND_DEST_ACTION(info) = None;
  } else {
    XDND_DEST_ACTION(info) = destAction;
  }
}

static void storeStatusMessageInfos(WMDraggingInfo *info, XClientMessageEvent *statusEvent)
{
  WMRect *noPosZone = &(XDND_NO_POS_ZONE(info));

#ifdef XDND_DEBUG

  traceStatusMsg(sourceScreen(info)->display, statusEvent);
#endif

  if (statusEvent->data.l[1] & 0x2UL) {
    /* bit 1 set: destination wants position messages on every move */
    noPosZone->size.width = 0;
    noPosZone->size.height = 0;
  } else {
    /* don't send another position message while in given rectangle */
    noPosZone->pos.x = statusEvent->data.l[2] >> 16;
    noPosZone->pos.y = statusEvent->data.l[2] & 0xFFFFL;
    noPosZone->size.width = statusEvent->data.l[3] >> 16;
    noPosZone->size.height = statusEvent->data.l[3] & 0xFFFFL;
  }

  if ((statusEvent->data.l[1] & 0x1L) || statusEvent->data.l[4] != None) {
    /* destination accept drop */
    storeDropAction(info, statusEvent->data.l[4]);
  } else {
    XDND_DEST_ACTION(info) = None;
  }
}

static void *idleState(WMView *view, XClientMessageEvent *event, WMDraggingInfo *info)
{
  WMScreen *scr;
  Atom destMsg = event->message_type;

  scr = WMViewScreen(view);

  if (destMsg == scr->xdndStatusAtom) {
    storeStatusMessageInfos(info, event);

    if (XDND_DEST_ACTION(info) != None) {
      recolorCursor(info, True);
      WMDragSourceStartTimer(info);
      return dropAllowedState;
    } else {
      /* drop denied */
      recolorCursor(info, False);
      return idleState;
    }
  }

  if (destMsg == scr->xdndFinishedAtom) {
    WMLogWarning("received xdndFinishedAtom before drop began");
  }

  WMDragSourceStartTimer(info);
  return idleState;
}

static void *dropAllowedState(WMView *view, XClientMessageEvent *event, WMDraggingInfo *info)
{
  WMScreen *scr = WMViewScreen(view);
  Atom destMsg = event->message_type;

  if (destMsg == scr->xdndStatusAtom) {
    storeStatusMessageInfos(info, event);

    if (XDND_DEST_ACTION(info) == None) {
      /* drop denied */
      recolorCursor(info, False);
      return idleState;
    }
  }

  WMDragSourceStartTimer(info);
  return dropAllowedState;
}

static void *finishDropState(WMView *view, XClientMessageEvent *event, WMDraggingInfo *info)
{
  WMScreen *scr = WMViewScreen(view);
  Atom destMsg = event->message_type;

  if (destMsg == scr->xdndFinishedAtom) {
    endDragProcess(info, True);
    return NULL;
  }

  WMDragSourceStartTimer(info);
  return finishDropState;
}

/* ----- end of source states ----- */

/* ----- source timer ----- */
static void dragSourceResponseTimeOut(CFRunLoopTimerRef timer, void *source)
{
  WMView *view = (WMView *) source;
  WMDraggingInfo *info = WMViewScreen(view)->dragInfo;

  WMLogWarning(_("delay for drag destination response expired"));
  sendLeaveMessage(info);

  recolorCursor(info, False);
  if (XDND_SOURCE_STATE(info) == finishDropState) {
    /* drop failed */
    endDragImage(info, True);
    endDragProcess(info, False);
  } else {
    XDND_SOURCE_STATE(info) = idleState;
  }
}

void WMDragSourceStopTimer()
{
  if (dndSourceTimer != NULL) {
    WMDeleteTimerHandler(dndSourceTimer);
    dndSourceTimer = NULL;
  }
}

void WMDragSourceStartTimer(WMDraggingInfo *info)
{
  WMDragSourceStopTimer();

  dndSourceTimer = WMAddTimerHandler(XDND_DESTINATION_RESPONSE_MAX_DELAY, 0,
                                     dragSourceResponseTimeOut, XDND_SOURCE_VIEW(info));
}

/* ----- End of Destination timer ----- */

void WMDragSourceStateHandler(WMDraggingInfo *info, XClientMessageEvent *event)
{
  WMView *view;
  WMDndState *newState;

  if (XDND_SOURCE_VIEW_STORED(info)) {
    if (XDND_SOURCE_STATE(info) != NULL) {
      view = XDND_SOURCE_VIEW(info);
#ifdef XDND_DEBUG

      printf("current source state: %s\n", stateName(XDND_SOURCE_STATE(info)));
#endif

      newState = (WMDndState *) XDND_SOURCE_STATE(info) (view, event, info);

#ifdef XDND_DEBUG

      printf("new source state: %s\n", stateName(newState));
#endif

      if (newState != NULL)
        XDND_SOURCE_STATE(info) = newState;
      /* else drop finished, and info has been flushed */
    }

  } else {
    WMLogWarning("received DnD message without having a target");
  }
}

void WMSetViewDragImage(WMView *view, WMPixmap *dragImage)
{
  if (view->dragImage != NULL)
    WMReleasePixmap(view->dragImage);

  view->dragImage = WMRetainPixmap(dragImage);
}

void WMReleaseViewDragImage(WMView *view)
{
  if (view->dragImage != NULL)
    WMReleasePixmap(view->dragImage);
}

/* Create a drag handler, associating drag event masks with dragEventProc */
void WMCreateDragHandler(WMView *view, WMEventProc *dragEventProc, void *clientData)
{
  WMCreateEventHandler(view,
                       ButtonPressMask | ButtonReleaseMask | Button1MotionMask, dragEventProc, clientData);
}

void WMDeleteDragHandler(WMView *view, WMEventProc *dragEventProc, void *clientData)
{
  WMDeleteEventHandler(view,
                       ButtonPressMask | ButtonReleaseMask | Button1MotionMask, dragEventProc, clientData);
}

/* set default drag handler for view */
void WMSetViewDraggable(WMView *view, WMDragSourceProcs *dragSourceProcs, WMPixmap *dragImage)
{
  wassertr(dragImage != NULL);
  view->dragImage = WMRetainPixmap(dragImage);

  WMSetViewDragSourceProcs(view, dragSourceProcs);

  WMCreateDragHandler(view, dragImageHandler, view);
}

void WMUnsetViewDraggable(WMView *view)
{
  if (view->dragSourceProcs) {
    wfree(view->dragSourceProcs);
    view->dragSourceProcs = NULL;
  }

  WMReleaseViewDragImage(view);

  WMDeleteDragHandler(view, dragImageHandler, view);
}
